// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/ozone/platform/drm/gpu/drm_device.h"

#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <utility>
#include <xf86drm.h>
#include <xf86drmMode.h>

#include "base/logging.h"
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/task_runner.h"
#include "base/thread_task_runner_handle.h"
#include "base/trace_event/trace_event.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "ui/display/types/gamma_ramp_rgb_entry.h"
#include "ui/ozone/platform/drm/common/drm_util.h"
#include "ui/ozone/platform/drm/gpu/hardware_display_plane_manager_legacy.h"

#if defined(USE_DRM_ATOMIC)
#include "ui/ozone/platform/drm/gpu/hardware_display_plane_manager_atomic.h"
#endif

namespace ui {

namespace {

    typedef base::Callback<void(uint32_t /* frame */,
        uint32_t /* seconds */,
        uint32_t /* useconds */,
        uint64_t /* id */)>
        DrmEventHandler;

    bool DrmCreateDumbBuffer(int fd,
        const SkImageInfo& info,
        uint32_t* handle,
        uint32_t* stride)
    {
        struct drm_mode_create_dumb request;
        memset(&request, 0, sizeof(request));
        request.width = info.width();
        request.height = info.height();
        request.bpp = info.bytesPerPixel() << 3;
        request.flags = 0;

        if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &request) < 0) {
            VPLOG(2) << "Cannot create dumb buffer";
            return false;
        }

        // The driver may choose to align the last row as well. We don't care about
        // the last alignment bits since they aren't used for display purposes, so
        // just check that the expected size is <= to what the driver allocated.
        DCHECK_LE(info.getSafeSize(request.pitch), request.size);

        *handle = request.handle;
        *stride = request.pitch;
        return true;
    }

    bool DrmDestroyDumbBuffer(int fd, uint32_t handle)
    {
        struct drm_mode_destroy_dumb destroy_request;
        memset(&destroy_request, 0, sizeof(destroy_request));
        destroy_request.handle = handle;
        return !drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy_request);
    }

    bool ProcessDrmEvent(int fd, const DrmEventHandler& callback)
    {
        char buffer[1024];
        int len = read(fd, buffer, sizeof(buffer));
        if (len == 0)
            return false;

        if (len < static_cast<int>(sizeof(drm_event))) {
            PLOG(ERROR) << "Failed to read DRM event";
            return false;
        }

        int idx = 0;
        while (idx < len) {
            DCHECK_LE(static_cast<int>(sizeof(drm_event)), len - idx);
            drm_event event;
            memcpy(&event, &buffer[idx], sizeof(event));
            switch (event.type) {
            case DRM_EVENT_FLIP_COMPLETE: {
                DCHECK_LE(static_cast<int>(sizeof(drm_event_vblank)), len - idx);
                drm_event_vblank vblank;
                memcpy(&vblank, &buffer[idx], sizeof(vblank));
                callback.Run(vblank.sequence, vblank.tv_sec, vblank.tv_usec,
                    vblank.user_data);
            } break;
            case DRM_EVENT_VBLANK:
                break;
            default:
                NOTREACHED();
                break;
            }

            idx += event.length;
        }

        return true;
    }

    bool CanQueryForResources(int fd)
    {
        drm_mode_card_res resources;
        memset(&resources, 0, sizeof(resources));
        // If there is no error getting DRM resources then assume this is a
        // modesetting device.
        return !drmIoctl(fd, DRM_IOCTL_MODE_GETRESOURCES, &resources);
    }

} // namespace

class DrmDevice::PageFlipManager {
public:
    PageFlipManager()
        : next_id_(0)
    {
    }
    ~PageFlipManager() { }

    void OnPageFlip(uint32_t frame,
        uint32_t seconds,
        uint32_t useconds,
        uint64_t id)
    {
        auto it = std::find_if(callbacks_.begin(), callbacks_.end(), FindCallback(id));
        if (it == callbacks_.end()) {
            LOG(WARNING) << "Could not find callback for page flip id=" << id;
            return;
        }

        DrmDevice::PageFlipCallback callback = it->callback;
        it->pending_calls -= 1;

        if (it->pending_calls)
            return;

        callbacks_.erase(it);
        callback.Run(frame, seconds, useconds);
    }

    uint64_t GetNextId() { return next_id_++; }

    void RegisterCallback(uint64_t id,
        uint64_t pending_calls,
        const DrmDevice::PageFlipCallback& callback)
    {
        callbacks_.push_back({ id, pending_calls, callback });
    }

private:
    struct PageFlip {
        uint64_t id;
        uint32_t pending_calls;
        DrmDevice::PageFlipCallback callback;
    };

    struct FindCallback {
        FindCallback(uint64_t id)
            : id(id)
        {
        }

        bool operator()(const PageFlip& flip) const { return flip.id == id; }

        uint64_t id;
    };

    uint64_t next_id_;

    std::vector<PageFlip> callbacks_;

    DISALLOW_COPY_AND_ASSIGN(PageFlipManager);
};

class DrmDevice::IOWatcher : public base::MessagePumpLibevent::Watcher {
public:
    IOWatcher(int fd, DrmDevice::PageFlipManager* page_flip_manager)
        : page_flip_manager_(page_flip_manager)
        , fd_(fd)
    {
        Register();
    }

    ~IOWatcher() override { Unregister(); }

private:
    void Register()
    {
        DCHECK(base::MessageLoopForIO::IsCurrent());
        base::MessageLoopForIO::current()->WatchFileDescriptor(
            fd_, true, base::MessageLoopForIO::WATCH_READ, &controller_, this);
    }

    void Unregister()
    {
        DCHECK(base::MessageLoopForIO::IsCurrent());
        controller_.StopWatchingFileDescriptor();
    }

    // base::MessagePumpLibevent::Watcher overrides:
    void OnFileCanReadWithoutBlocking(int fd) override
    {
        DCHECK(base::MessageLoopForIO::IsCurrent());
        TRACE_EVENT1("drm", "OnDrmEvent", "socket", fd);

        if (!ProcessDrmEvent(fd, base::Bind(&DrmDevice::PageFlipManager::OnPageFlip, base::Unretained(page_flip_manager_))))
            Unregister();
    }

    void OnFileCanWriteWithoutBlocking(int fd) override { NOTREACHED(); }

    DrmDevice::PageFlipManager* page_flip_manager_;

    base::MessagePumpLibevent::FileDescriptorWatcher controller_;

    int fd_;

    DISALLOW_COPY_AND_ASSIGN(IOWatcher);
};

DrmDevice::DrmDevice(const base::FilePath& device_path,
    base::File file,
    bool is_primary_device)
    : device_path_(device_path)
    , file_(std::move(file))
    , page_flip_manager_(new PageFlipManager())
    , is_primary_device_(is_primary_device)
{
}

DrmDevice::~DrmDevice() { }

bool DrmDevice::Initialize(bool use_atomic)
{
    // Ignore devices that cannot perform modesetting.
    if (!CanQueryForResources(file_.GetPlatformFile())) {
        VLOG(2) << "Cannot query for resources for '" << device_path_.value()
                << "'";
        return false;
    }

#if defined(USE_DRM_ATOMIC)
    // Use atomic only if the build, kernel & flags all allow it.
    if (use_atomic && SetCapability(DRM_CLIENT_CAP_ATOMIC, 1))
        plane_manager_.reset(new HardwareDisplayPlaneManagerAtomic());
#endif // defined(USE_DRM_ATOMIC)

    if (!plane_manager_)
        plane_manager_.reset(new HardwareDisplayPlaneManagerLegacy());
    if (!plane_manager_->Initialize(this)) {
        LOG(ERROR) << "Failed to initialize the plane manager for "
                   << device_path_.value();
        plane_manager_.reset();
        return false;
    }

    watcher_.reset(
        new IOWatcher(file_.GetPlatformFile(), page_flip_manager_.get()));

    return true;
}

ScopedDrmCrtcPtr DrmDevice::GetCrtc(uint32_t crtc_id)
{
    DCHECK(file_.IsValid());
    return ScopedDrmCrtcPtr(drmModeGetCrtc(file_.GetPlatformFile(), crtc_id));
}

bool DrmDevice::SetCrtc(uint32_t crtc_id,
    uint32_t framebuffer,
    std::vector<uint32_t> connectors,
    drmModeModeInfo* mode)
{
    DCHECK(file_.IsValid());
    DCHECK(!connectors.empty());
    DCHECK(mode);

    TRACE_EVENT2("drm", "DrmDevice::SetCrtc", "crtc", crtc_id, "size",
        gfx::Size(mode->hdisplay, mode->vdisplay).ToString());
    return !drmModeSetCrtc(file_.GetPlatformFile(), crtc_id, framebuffer, 0, 0,
        connectors.data(), connectors.size(), mode);
}

bool DrmDevice::SetCrtc(drmModeCrtc* crtc, std::vector<uint32_t> connectors)
{
    DCHECK(file_.IsValid());
    // If there's no buffer then the CRTC was disabled.
    if (!crtc->buffer_id)
        return DisableCrtc(crtc->crtc_id);

    DCHECK(!connectors.empty());

    TRACE_EVENT1("drm", "DrmDevice::RestoreCrtc", "crtc", crtc->crtc_id);
    return !drmModeSetCrtc(file_.GetPlatformFile(), crtc->crtc_id,
        crtc->buffer_id, crtc->x, crtc->y, connectors.data(),
        connectors.size(), &crtc->mode);
}

bool DrmDevice::DisableCrtc(uint32_t crtc_id)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT1("drm", "DrmDevice::DisableCrtc", "crtc", crtc_id);
    return !drmModeSetCrtc(file_.GetPlatformFile(), crtc_id, 0, 0, 0, NULL, 0,
        NULL);
}

ScopedDrmConnectorPtr DrmDevice::GetConnector(uint32_t connector_id)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT1("drm", "DrmDevice::GetConnector", "connector", connector_id);
    return ScopedDrmConnectorPtr(
        drmModeGetConnector(file_.GetPlatformFile(), connector_id));
}

bool DrmDevice::AddFramebuffer2(uint32_t width,
    uint32_t height,
    uint32_t format,
    uint32_t handles[4],
    uint32_t strides[4],
    uint32_t offsets[4],
    uint32_t* framebuffer,
    uint32_t flags)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT1("drm", "DrmDevice::AddFramebuffer", "handle", handles[0]);
    return !drmModeAddFB2(file_.GetPlatformFile(), width, height, format, handles,
        strides, offsets, framebuffer, flags);
}

bool DrmDevice::RemoveFramebuffer(uint32_t framebuffer)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT1("drm", "DrmDevice::RemoveFramebuffer", "framebuffer",
        framebuffer);
    return !drmModeRmFB(file_.GetPlatformFile(), framebuffer);
}

bool DrmDevice::PageFlip(uint32_t crtc_id,
    uint32_t framebuffer,
    const PageFlipCallback& callback)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT2("drm", "DrmDevice::PageFlip", "crtc", crtc_id, "framebuffer",
        framebuffer);

    // NOTE: Calling drmModeSetCrtc will immediately update the state, though
    // callbacks to already scheduled page flips will be honored by the kernel.
    uint64_t id = page_flip_manager_->GetNextId();
    if (!drmModePageFlip(file_.GetPlatformFile(), crtc_id, framebuffer,
            DRM_MODE_PAGE_FLIP_EVENT, reinterpret_cast<void*>(id))) {
        // If successful the payload will be removed by a PageFlip event.
        page_flip_manager_->RegisterCallback(id, 1, callback);
        return true;
    }

    return false;
}

bool DrmDevice::PageFlipOverlay(uint32_t crtc_id,
    uint32_t framebuffer,
    const gfx::Rect& location,
    const gfx::Rect& source,
    int overlay_plane)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT2("drm", "DrmDevice::PageFlipOverlay", "crtc", crtc_id,
        "framebuffer", framebuffer);
    return !drmModeSetPlane(file_.GetPlatformFile(), overlay_plane, crtc_id,
        framebuffer, 0, location.x(), location.y(),
        location.width(), location.height(), source.x(),
        source.y(), source.width(), source.height());
}

ScopedDrmFramebufferPtr DrmDevice::GetFramebuffer(uint32_t framebuffer)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT1("drm", "DrmDevice::GetFramebuffer", "framebuffer", framebuffer);
    return ScopedDrmFramebufferPtr(
        drmModeGetFB(file_.GetPlatformFile(), framebuffer));
}

ScopedDrmPropertyPtr DrmDevice::GetProperty(drmModeConnector* connector,
    const char* name)
{
    TRACE_EVENT2("drm", "DrmDevice::GetProperty", "connector",
        connector->connector_id, "name", name);
    for (int i = 0; i < connector->count_props; ++i) {
        ScopedDrmPropertyPtr property(
            drmModeGetProperty(file_.GetPlatformFile(), connector->props[i]));
        if (!property)
            continue;

        if (strcmp(property->name, name) == 0)
            return property;
    }

    return ScopedDrmPropertyPtr();
}

bool DrmDevice::SetProperty(uint32_t connector_id,
    uint32_t property_id,
    uint64_t value)
{
    DCHECK(file_.IsValid());
    return !drmModeConnectorSetProperty(file_.GetPlatformFile(), connector_id,
        property_id, value);
}

bool DrmDevice::GetCapability(uint64_t capability, uint64_t* value)
{
    DCHECK(file_.IsValid());
    return !drmGetCap(file_.GetPlatformFile(), capability, value);
}

ScopedDrmPropertyBlobPtr DrmDevice::GetPropertyBlob(drmModeConnector* connector,
    const char* name)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT2("drm", "DrmDevice::GetPropertyBlob", "connector",
        connector->connector_id, "name", name);
    for (int i = 0; i < connector->count_props; ++i) {
        ScopedDrmPropertyPtr property(
            drmModeGetProperty(file_.GetPlatformFile(), connector->props[i]));
        if (!property)
            continue;

        if (strcmp(property->name, name) == 0 && (property->flags & DRM_MODE_PROP_BLOB))
            return ScopedDrmPropertyBlobPtr(drmModeGetPropertyBlob(
                file_.GetPlatformFile(), connector->prop_values[i]));
    }

    return ScopedDrmPropertyBlobPtr();
}

bool DrmDevice::SetCursor(uint32_t crtc_id,
    uint32_t handle,
    const gfx::Size& size)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT2("drm", "DrmDevice::SetCursor", "crtc_id", crtc_id, "handle",
        handle);
    return !drmModeSetCursor(file_.GetPlatformFile(), crtc_id, handle,
        size.width(), size.height());
}

bool DrmDevice::MoveCursor(uint32_t crtc_id, const gfx::Point& point)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT1("drm", "DrmDevice::MoveCursor", "crtc_id", crtc_id);
    return !drmModeMoveCursor(file_.GetPlatformFile(), crtc_id, point.x(),
        point.y());
}

bool DrmDevice::CreateDumbBuffer(const SkImageInfo& info,
    uint32_t* handle,
    uint32_t* stride)
{
    DCHECK(file_.IsValid());

    TRACE_EVENT0("drm", "DrmDevice::CreateDumbBuffer");
    return DrmCreateDumbBuffer(file_.GetPlatformFile(), info, handle, stride);
}

bool DrmDevice::DestroyDumbBuffer(uint32_t handle)
{
    DCHECK(file_.IsValid());
    TRACE_EVENT1("drm", "DrmDevice::DestroyDumbBuffer", "handle", handle);
    return DrmDestroyDumbBuffer(file_.GetPlatformFile(), handle);
}

bool DrmDevice::MapDumbBuffer(uint32_t handle, size_t size, void** pixels)
{
    struct drm_mode_map_dumb map_request;
    memset(&map_request, 0, sizeof(map_request));
    map_request.handle = handle;
    if (drmIoctl(file_.GetPlatformFile(), DRM_IOCTL_MODE_MAP_DUMB,
            &map_request)) {
        PLOG(ERROR) << "Cannot prepare dumb buffer for mapping";
        return false;
    }

    *pixels = mmap(0, size, PROT_READ | PROT_WRITE, MAP_SHARED,
        file_.GetPlatformFile(), map_request.offset);
    if (*pixels == MAP_FAILED) {
        PLOG(ERROR) << "Cannot mmap dumb buffer";
        return false;
    }

    return true;
}

bool DrmDevice::UnmapDumbBuffer(void* pixels, size_t size)
{
    return !munmap(pixels, size);
}

bool DrmDevice::CloseBufferHandle(uint32_t handle)
{
    struct drm_gem_close close_request;
    memset(&close_request, 0, sizeof(close_request));
    close_request.handle = handle;
    return !drmIoctl(file_.GetPlatformFile(), DRM_IOCTL_GEM_CLOSE,
        &close_request);
}

bool DrmDevice::CommitProperties(drmModeAtomicReq* properties,
    uint32_t flags,
    uint32_t crtc_count,
    const PageFlipCallback& callback)
{
#if defined(USE_DRM_ATOMIC)
    uint64_t id = 0;
    bool page_flip_event_requested = flags & DRM_MODE_PAGE_FLIP_EVENT;

    if (page_flip_event_requested)
        id = page_flip_manager_->GetNextId();

    if (!drmModeAtomicCommit(file_.GetPlatformFile(), properties, flags,
            reinterpret_cast<void*>(id))) {
        if (page_flip_event_requested)
            page_flip_manager_->RegisterCallback(id, crtc_count, callback);

        return true;
    }
#endif // defined(USE_DRM_ATOMIC)
    return false;
}

bool DrmDevice::SetCapability(uint64_t capability, uint64_t value)
{
    DCHECK(file_.IsValid());
    return !drmSetClientCap(file_.GetPlatformFile(), capability, value);
}

bool DrmDevice::SetMaster()
{
    TRACE_EVENT1("drm", "DrmDevice::SetMaster", "path", device_path_.value());
    DCHECK(file_.IsValid());
    return (drmSetMaster(file_.GetPlatformFile()) == 0);
}

bool DrmDevice::DropMaster()
{
    TRACE_EVENT1("drm", "DrmDevice::DropMaster", "path", device_path_.value());
    DCHECK(file_.IsValid());
    return (drmDropMaster(file_.GetPlatformFile()) == 0);
}

bool DrmDevice::SetGammaRamp(uint32_t crtc_id,
    const std::vector<GammaRampRGBEntry>& lut)
{
    ScopedDrmCrtcPtr crtc = GetCrtc(crtc_id);

    // TODO(robert.bradford) resample the incoming ramp to match what the kernel
    // expects.
    if (static_cast<size_t>(crtc->gamma_size) != lut.size()) {
        LOG(ERROR) << "Gamma table size mismatch: supplied " << lut.size()
                   << " expected " << crtc->gamma_size;
    }

    std::vector<uint16_t> r, g, b;
    r.reserve(lut.size());
    g.reserve(lut.size());
    b.reserve(lut.size());

    for (size_t i = 0; i < lut.size(); ++i) {
        r.push_back(lut[i].r);
        g.push_back(lut[i].g);
        b.push_back(lut[i].b);
    }

    DCHECK(file_.IsValid());
    TRACE_EVENT0("drm", "DrmDevice::SetGamma");
    return (drmModeCrtcSetGamma(file_.GetPlatformFile(), crtc_id, r.size(), &r[0],
                &g[0], &b[0])
        == 0);
}

} // namespace ui
